﻿using System;
using System.Collections.Generic;
using System.Collections;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using BesAsm.MasterData.Entities;
using BesAsm.MasterData.Gis;
using BesAsm.Framework.ArcGisUtilities;
using BesAsm.Framework.Tracer;
using BesAsm.Framework.Reporting;
using ESRI.ArcGIS.Framework;
using ESRI.ArcGIS.Carto;
using ESRI.ArcGIS.ArcMapUI;
using ESRI.ArcGIS.Geodatabase;

namespace BesAsm.Emgaats.Core
{
  /// <summary>
  /// Delegate for events related to layers checking in our out of the library
  /// </summary>
  /// <param name="sender">The object raising the event</param>
  /// <param name="e">A CheckInCheckOutArgs identifying which layer was checked in or out</param>
  public delegate void CheckInCheckOutEventHandler(object sender, CheckInCheckOutArgs e);

  /// <summary>
  /// The LayerLibrarian provides notification when system feature classes are added or removed
  /// from an ArcGIS session. The LayerLibrarian maintains two collections: A catalog of all possible LibraryItems 
  /// and their underlying name/data source; and a library of LibraryItems which are available for check-out.
  /// The Librarian is responsible for raising events when LibraryItems are checked in or out of the 
  /// Library.
  /// </summary>
  public sealed class LayerLibrarian : IEnumerable
  {
    private static readonly LayerLibrarian _librarian = new LayerLibrarian();

    private ArcGisUtils _arcUtils;

    private string _modelDataSource;

    private Dictionary<LibraryItem, string> _layerCatalog;
    private EmgaatsLayerCollection _library;

    private bool _masterDataLoaded, _modelDataLoaded = false;

    private IActiveViewEvents_Event activeViewEvents;

    private static bool initialized = false;

    /// <summary>
    /// Raised when a system layer is added to the ArcGIS session, either by the user or programmatically
    /// </summary>
    public static event CheckInCheckOutEventHandler CheckedIn;
    /// <summary>
    /// Raised when a system layer is removed from the ArcGIS session, either by the user or programmatically
    /// </summary>
    public static event CheckInCheckOutEventHandler CheckedOut;

    /// <summary>
    /// Occurs when all master-level data is available in the LayerLibrarian
    /// </summary>
    public event EventHandler MasterDataLoaded;
    /// <summary>
    /// Occurs when master-level data is no longer available in the LayerLibrarian
    /// </summary>
    public event EventHandler MasterDataUnloaded;
    /// <summary>
    /// Occurs when all model-level data is available in the LayerLibrarian
    /// </summary>
    public event EventHandler ModelDataLoaded;
    /// <summary>
    /// Occurs when model-level data is no longer available in the LayerLibrarian
    /// </summary>
    public event EventHandler ModelDataUnloaded;

    /// <summary>
    /// Provides access to the LayerLibrarian for any IEmgaatsExtension
    /// </summary>
    /// <param name="extension">The extension requesting access to the LayerLibrarian</param>
    /// <returns>A singleton instance of the LayerLibrarian</returns>
    public static LayerLibrarian AttachToLibrarian(IEmgaatsExtension extension)
    {
      return _librarian;
    }

    private LayerLibrarian()
    {
    }

    /// <summary>
    /// Any initialization logic for the LayeLibrarian
    /// </summary>
    /// <param name="app"></param>
    internal static LayerLibrarian InitializeLibrarian()
    {
      _librarian._arcUtils = new ArcGisUtils(ArcMap.Application);

      _librarian._library = new EmgaatsLayerCollection();
      _librarian._layerCatalog = new Dictionary<LibraryItem, string>();

      ArcMap.Events.NewDocument += new IDocumentEvents_NewDocumentEventHandler(_librarian.OnNewDocument);
      ArcMap.Events.OpenDocument += new IDocumentEvents_OpenDocumentEventHandler(_librarian.OnOpenDocument);

      ConfigureDatabaseConnection();
      _librarian.ConfigureArcGisConnection();

      _librarian.InitializeCardCatalog();

      return _librarian;
    }

    private static void ConfigureDatabaseConnection()
    {
      if (Properties.Settings.Default.UserConnectionString != string.Empty)
        // We can cheat here by directly accessing the ConnectionString application setting
        Properties.Settings.Default["ConnectionString"] =
          Properties.Settings.Default.UserConnectionString;

      ProviderInitializer.InitializeProvider(Properties.Settings.Default.ConnectionString);
    }

    private void ConfigureArcGisConnection()
    {
      string sdeConnectionFile = MasterDataSource;

      if (System.IO.File.Exists(sdeConnectionFile))
        return;

      System.IO.Stream stream =
        System.IO.File.Open(sdeConnectionFile, System.IO.FileMode.Create);

      System.IO.BinaryWriter writer = new System.IO.BinaryWriter(stream);
      writer.Write(Properties.Resources.DefaultArcCatalogConnectionFile);
    }

    /// <summary>
    /// Reads the data sources and data names for all system layers
    /// </summary>
    private void InitializeCardCatalog()
    {
      //DONE: Figure out where _modelDataSource should be set 
      //ModelDataSource is set after the build of the model (build command tells the model
      //builder extension to set the Librarian's model data source
      _modelDataSource = "";

      //TODO: Read layer names from config file
      _layerCatalog = new Dictionary<LibraryItem, string>();

      _layerCatalog.Add(LibraryItem.MasterLinks, "ASM_MASTER_RPT_SDE.GIS.LINKS");
      _layerCatalog.Add(LibraryItem.MasterNodes, "ASM_MASTER_RPT_SDE.GIS.NODES");
      _layerCatalog.Add(LibraryItem.MasterDscs, "ASM_MASTER_RPT_SDE.GIS.DSCS");
      _layerCatalog.Add(LibraryItem.MasterSscs, "ASM_MASTER_RPT_SDE.GIS.SSCS");
      _layerCatalog.Add(LibraryItem.MasterParkingTargets, "ASM_MASTER_RPT_SDE.GIS.PARKING_TARGETS");
      _layerCatalog.Add(LibraryItem.MasterRoofTargets, "ASM_MASTER_RPT_SDE.GIS.ROOF_TARGETS");
      _layerCatalog.Add(LibraryItem.MasterStreetTargets, "ASM_MASTER_RPT_SDE.GIS.STREET_TARGETS");

      _layerCatalog.Add(LibraryItem.ModelLinks, "MODEL_LINKS");
      _layerCatalog.Add(LibraryItem.ModelNodes, "MODEL_NODES");
      _layerCatalog.Add(LibraryItem.ModelDscs, "MODEL_DSCS");
      _layerCatalog.Add(LibraryItem.ModelSscs, "MODEL_SSCS");
      _layerCatalog.Add(LibraryItem.ModelParkingTargets, "MODEL_PARKING_TARGETS");
      _layerCatalog.Add(LibraryItem.ModelRoofTargets, "MODEL_ROOF_TARGETS");
      _layerCatalog.Add(LibraryItem.ModelStreetTargets, "MODEL_STREET_TARGETS");

      _layerCatalog.Add(LibraryItem.StartLinks, "Start Links");
      _layerCatalog.Add(LibraryItem.StopLinks, "Stop Links");
      _layerCatalog.Add(LibraryItem.TraceLinks, "Trace Links");
    }

    /// <summary>
    /// The NewDocument event handler. 
    /// </summary>
    /// <remarks></remarks>
    protected void OnNewDocument()
    {
      ConfigureActiveViewEvents();
      RebuildLibrary();
    }

    /// <summary>
    /// The OpenDocument event handler.
    /// </summary>
    /// <remarks></remarks>
    protected void OnOpenDocument()
    {
      ConfigureActiveViewEvents();
      RebuildLibrary();
    }

    #region ActiveView events for handling layers added/removed to the ArcGIS session
    /// <summary>
    /// Configures events which handle layers being added to and removed from the ArcGIS session
    /// </summary>
    private void ConfigureActiveViewEvents()
    {
      IActiveView activeView = (IActiveView)ArcMap.Document.ActiveView;
      IMap map = (IMap)activeView.FocusMap;

      activeViewEvents = (IActiveViewEvents_Event)map;
      activeViewEvents.ItemAdded += new IActiveViewEvents_ItemAddedEventHandler(OnActiveViewEventsItemAdded);
      activeViewEvents.ItemDeleted += new IActiveViewEvents_ItemDeletedEventHandler(OnActiveViewEventsItemDeleted);
    }

    /// <summary>
    /// Triggered when a new layer is added to ArcGIS
    /// </summary>
    /// <param name="item">The layer that was added</param>
    private void OnActiveViewEventsItemAdded(object item)
    {
      if (item is IFeatureLayer)
      {
        IFeatureLayer fl = item as IFeatureLayer;
        if (IsLibraryItem(fl))
          AddToLibrary(fl);
      }
      else if (item is IGroupLayer)
      {
        ICompositeLayer compositeLayer = item as ICompositeLayer;
        AddGroupToLibrary(compositeLayer);
      }
    }

    /// <summary>
    /// Triggered when a layer is remvoed from ArcGIS
    /// </summary>
    /// <param name="item">The layer that was removed</param>
    private void OnActiveViewEventsItemDeleted(object item)
    {
      if (item is IFeatureLayer)
      {
        IFeatureLayer fl = item as IFeatureLayer;
        if (IsLibraryItem(fl))
          RemoveFromLibrary(fl);
      }
      else if (item is IGroupLayer)
      {
        ICompositeLayer compositeLayer = item as ICompositeLayer;
        RemoveGroupFromLibrary(compositeLayer);
      }

    }

    private void OnActiveViewEventsContentsChanged()
    {
      RebuildLibrary();
    }

    private void OnActiveViewEventsContentsCleared()
    {
      ClearLibrary();
    }
    #endregion

    /// <summary>
    /// Determines if an IFeatureLayer is an Emgaats system layer based on the data name
    /// and data source
    /// </summary>
    /// <param name="fl">The layer to evaluate</param>
    /// <returns>True if fl is an Emgaats system layer</returns>
    public bool IsLibraryItem(IFeatureLayer fl)
    {
      if (fl == null || fl.FeatureClass == null) return false;

      IDataset ds = fl.FeatureClass as IDataset;
      string dataSource;

      //Any layer using one of the reserved catalog names is automatically assumed to be a LibraryItem
      if (ReverseLookupCatalog.ContainsKey(fl.Name))
        return true;

      if (fl.DataSourceType == "SDE Feature Class")
        dataSource = ds.Workspace.ConnectionProperties.GetProperty("DATABASE").ToString();
      else
        dataSource = ds.Workspace.PathName;

      if (dataSource != Properties.Settings.Default.MasterDatabaseName && dataSource != _modelDataSource)
        return false;

      if (!_layerCatalog.ContainsValue(ds.Name))
        return false;

      return true;
    }

    private void AddToLibrary(IFeatureLayer layer)
    {
      LibraryItem libraryItem;
      try
      {
        IDataset ds = (IDataset)layer.FeatureClass;

        string lookupName;
        if (ReverseLookupCatalog.ContainsKey(layer.Name))
          lookupName = layer.Name;
        else
          lookupName = ds.Name;

        libraryItem = _layerCatalog.Single(p => p.Value == lookupName).Key;
        if (_library.Contains(libraryItem))
        {
          throw new Exception("Item already in Library");
        }

        EmgaatsLayer emgLayer = new EmgaatsLayer(libraryItem, layer);
        _library.Add(emgLayer);
        ApplyMasterSymbology(emgLayer);
        OnCheckedIn(new CheckInCheckOutArgs(emgLayer));

        bool masterDataLoaded = true; ;
        if (MasterLibraryItems.Contains(libraryItem))
        {
          foreach (LibraryItem masterItem in MasterLibraryItems)
          {
            if (!this.Contains(masterItem))
            {
              masterDataLoaded = false;
              break;
            }
          }

          if (masterDataLoaded)
            OnMasterDataLoaded(EventArgs.Empty);
        }

        bool modelDataLoaded = true;
        if (ModelLibraryItems.Contains(libraryItem))
        {
          _modelDataLoaded = true;
          foreach (LibraryItem modelItem in ModelLibraryItems)
          {
            if (!this.Contains(modelItem))
            {
              modelDataLoaded = false;
              break;
            }
          }

          if (modelDataLoaded)
            OnModelDataLoaded(EventArgs.Empty);
        }
      }
      catch (Exception ex)
      {
        Messenger.ReportMessage("Layer Librarian failed to add layer '"
          + layer.Name + "': " + ex.Message, ReportableMessageType.Error);
        return;
      }

    }

    private void AddGroupToLibrary(ICompositeLayer compositeLayer)
    {
      for (int i = 0; i < compositeLayer.Count; i++)
      {
        ILayer layer = compositeLayer.get_Layer(i);
        if (layer is IFeatureLayer)
        {
          IFeatureLayer featureLayer = (IFeatureLayer)layer;
          if (IsLibraryItem(featureLayer))
            AddToLibrary(featureLayer);
        }
        else if (layer is IGroupLayer)
        {
          AddGroupToLibrary(layer as ICompositeLayer);
        }
      }
    }

    private void ApplyMasterSymbology(IGeoFeatureLayer layer)
    {
      string layerName = layer.Name;
      string lyrFile = Properties.Settings.Default.MasterLayersPath + layerName + ".lyr";
      if (System.IO.File.Exists(lyrFile))
        layer.ApplyLayerFile(lyrFile);
      else
        return; //TODO: log warning if default layer file not found
    }

    private void RemoveFromLibrary(IFeatureLayer layer)
    {
      LibraryItem libraryItem;
      try
      {
        IDataset ds = (IDataset)layer.FeatureClass;

        string lookupName;
        if (ReverseLookupCatalog.ContainsKey(layer.Name))
          lookupName = layer.Name;
        else
          lookupName = ds.Name;


        libraryItem = _layerCatalog.First(p => p.Value == lookupName).Key;
        if (!_library.Contains(libraryItem))
        {
          throw new Exception("Item not in Library");
        }
      }
      catch (Exception ex)
      {
        Messenger.ReportMessage("Layer Librarian failed to remove layer '"
          + layer.Name + "': " + ex.Message, ReportableMessageType.Error);
        return;
      }

      EmgaatsLayer removedLayer = _library[libraryItem];

      _library.Remove(libraryItem);
      OnCheckedOut(new CheckInCheckOutArgs(removedLayer));

      if (_masterDataLoaded && MasterLibraryItems.Contains(libraryItem))
        OnMasterDataUnloaded(EventArgs.Empty);
      else if (_modelDataLoaded && ModelLibraryItems.Contains(libraryItem))
        OnModelDataUnloaded(EventArgs.Empty);
    }

    private void RemoveGroupFromLibrary(ICompositeLayer compositeLayer)
    {
      for (int i = 0; i < compositeLayer.Count; i++)
      {
        ILayer layer = compositeLayer.get_Layer(i);
        if (layer is IFeatureLayer)
        {
          IFeatureLayer featureLayer = (IFeatureLayer)layer;
          if (IsLibraryItem(featureLayer))
            RemoveFromLibrary(featureLayer);
        }
        else if (layer is ICompositeLayer)
        {
          RemoveGroupFromLibrary(layer as ICompositeLayer);
        }
      }
    }

    private void ClearLibrary()
    {
      EmgaatsLayer[] layers = _library.ToArray();
      int count = _library.Count;
      for (int i = 0; i < count; i++)
      {
        RemoveFromLibrary(layers[i]);
        layers[i] = null;
      }
    }

    private void RebuildLibrary()
    {
      ClearLibrary();
      List<ILayer> layers = _arcUtils.GetAllLayersInMap();

      foreach (ILayer layer in layers)
      {
        if (layer is IFeatureLayer)
        {
          IFeatureLayer fl = layer as IFeatureLayer;
          if (fl == null)
            continue;
          if (IsLibraryItem(fl))
            AddToLibrary(fl);
        }
      }
      return;
    }

    private void OnCheckedIn(CheckInCheckOutArgs e)
    {
      if (CheckedIn != null)
        CheckedIn(this, e);
    }

    private void OnCheckedOut(CheckInCheckOutArgs e)
    {
      if (CheckedOut != null)
        CheckedOut(this, e);
    }

    private void OnMasterDataLoaded(EventArgs e)
    {
      _masterDataLoaded = true;
      if (MasterDataLoaded != null)
        MasterDataLoaded(this, e);
    }
    private void OnMasterDataUnloaded(EventArgs e)
    {
      _masterDataLoaded = false;
      if (MasterDataUnloaded != null)
        MasterDataUnloaded(this, e);
    }
    private void OnModelDataLoaded(EventArgs e)
    {
      _modelDataLoaded = true;
      if (ModelDataLoaded != null)
        ModelDataLoaded(this, e);
    }
    private void OnModelDataUnloaded(EventArgs e)
    {
      _modelDataLoaded = false;
      if (ModelDataUnloaded != null)
        ModelDataUnloaded(this, e);
    }

    /// <summary>
    /// Returns a string indicating the master data source
    /// </summary>
    public string MasterDataSource
    {
      get
      {
        return System.IO.Path.Combine(
          _arcUtils.GetCatalogUserDataPath(),
          Properties.Settings.Default.ArcCatalogConnectionFileName);
      }
    }

    /// <summary>
    /// Returns or sets a string indicating the model data source
    /// </summary>
    public string ModelDataSource
    {
      get
      {
        return _modelDataSource;
      }
      set
      {
        _modelDataSource = value;
      }
    }

    /// <summary>
    /// Returns the name of a system layer specified by a LibraryItem
    /// </summary>
    /// <param name="libraryItem">A LibraryItem</param>
    /// <returns>The name of the LibraryItem</returns>
    public string GetDataName(LibraryItem libraryItem)
    {
      return _layerCatalog[libraryItem];
    }

    public IDictionary<LibraryItem, string> LayerCatalog
    {
      get { return _layerCatalog.ToDictionary(p => p.Key, p => p.Value); }
    }

    public IDictionary<string, LibraryItem> ReverseLookupCatalog
    {
      get { return _layerCatalog.ToDictionary(p => p.Value, p => p.Key); }
    }

    /// <summary>
    /// Gets a copy of the library contents.
    /// </summary>
    public static EmgaatsLayerCollection Library
    {
      get { return new EmgaatsLayerCollection(_librarian._library); }
    }

    /// <summary>
    /// Determines whether the specified library item is available
    /// </summary>
    /// <param name="libraryItem">The library item.</param>
    /// <returns>
    ///   <c>true</c> if the specified library item is available otherwise, <c>false</c>.
    /// </returns>
    public bool Contains(LibraryItem libraryItem)
    {
      return _library.Contains(libraryItem);
    }

    /// <summary>
    /// Gets the <see cref="BesAsm.Emgaats.Core.EmgaatsLayer"/> with the specified library item.
    /// </summary>
    public EmgaatsLayer this[LibraryItem libraryItem]
    {
      get { return _library[libraryItem]; }
    }

    /// <summary>
    /// Gets a value indicating whether all master data is loaded into the LayerLibrarian.
    /// </summary>
    /// <value>
    /// 	<c>true</c> if this all master data is loaded into the LayerLibrarian; otherwise, <c>false</c>.
    /// </value>
    public static bool IsMasterDataLoaded
    {
      get { return LayerLibrarian._librarian._masterDataLoaded; }
    }

    /// <summary>
    /// Gets a value indicating whether all model data is loaded into the LayerLibrarian.
    /// </summary>
    /// <value>
    /// 	<c>true</c> if all model data is loaded into the LayerLibrarian; otherwise, <c>false</c>.
    /// </value>
    public static bool IsModelDataLoaded
    {
      get { return LayerLibrarian._librarian._modelDataLoaded; }
    }

    /// <summary>
    /// Returns an array of all LibraryItems which are classified as master-level data
    /// </summary>
    public static LibraryItem[] MasterLibraryItems = new LibraryItem[] 
    { 
      LibraryItem.MasterDscs,
      LibraryItem.MasterLinks,
      LibraryItem.MasterNodes,
      LibraryItem.MasterParkingTargets,
      LibraryItem.MasterRoofTargets,
      LibraryItem.MasterSscs,
      LibraryItem.MasterStreetTargets
    };

    /// <summary>
    /// Returns an array of all LibraryItems which are classified as model-level data
    /// </summary>
    public static LibraryItem[] ModelLibraryItems = new LibraryItem[]
    {
      LibraryItem.ModelDscs,
      LibraryItem.ModelLinks,
      LibraryItem.ModelNodes,
      LibraryItem.ModelParkingTargets,
      LibraryItem.ModelRoofTargets,
      LibraryItem.ModelSscs,
      LibraryItem.ModelStreetTargets
    };

    /// <summary>
    /// Returns an array of all LibraryItems which are classified as tracer-level data
    /// </summary>
    public static LibraryItem[] TracerLibraryItems = new LibraryItem[]
    {
      LibraryItem.StartLinks,
      LibraryItem.StopLinks,
      LibraryItem.TraceLinks
    };

    #region IEnumerable Members

    public IEnumerator GetEnumerator()
    {
      return (_library as IEnumerable).GetEnumerator();
    }

    #endregion


  } // Librarian


}
